#region Using Statements
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using SpaceGame.Gameplay;
using SpaceGame.Gameplay.AI;
using SpaceGame.Gameplay.AI.Pathfinding;
using SpaceGame.Rendering;
using SpaceGame.Simulation;
#endregion

namespace SpaceGame
{
    /// <summary>
    /// Owns all game state and executes all game-wide logic.
    /// </summary>
    public class World
    {
        #region Constants
        /// <summary>
        /// The number of seconds before the first power-up appears in a game.
        /// </summary>
        const float initialPowerUpDelay = 10f;

        /// <summary>
        /// The time between each power-up spawn.
        /// </summary>
        const float powerUpDelay = 10f;

        /// <summary>
        /// The number of stars to generate in the starfield.
        /// </summary>
        const int starCount = 1028;

        /// <summary>
        /// How far starfield should generate outside the dimensions of the game field.
        /// </summary>
        const int starfieldBuffer = 512;

        /// <summary>
        /// The colors used for each ship, given it's player-index.
        /// </summary>
        static readonly Color[] shipColorsByPlayerIndex = 
            {
                Color.Lime, Color.CornflowerBlue, Color.Fuchsia, Color.Red
            };
        #endregion

        #region Fields
        readonly Random random = new Random();

        /// <summary>
        /// The dimensions of the game board.
        /// </summary>
        Vector2 dimensions;

        /// <summary>
        /// The safe dimensions of the game board.
        /// </summary>
        Rectangle safeDimensions;

        /// <summary>
        /// The timer to see if another power-up can arrive.
        /// </summary>
        float powerUpTimer;
        
        /// <summary>
        /// The walls in the game.
        /// </summary>
        Vector2[] walls;

        /// <summary>
        /// Navigation mesh representation of the game.
        /// </summary>
        NavigationMesh navigationMesh;

        /// <summary>
        /// The starfield effect behind the game-board.
        /// </summary>
        Starfield starfield;

        /// <summary>
        /// The world entity represents the world in the sense of collision against walls.
        /// </summary>
        WorldEntity worldEntity;

        /// <summary>
        /// Cached list of collision results, for more optimal collision detection.
        /// </summary>
        readonly List<CollisionResult> collisionResults = new List<CollisionResult>();
        #endregion

        #region Properties

        //public virtual AudioManager AudioManager { get; set; }

        /// <summary>
        /// Handles effects throught this world
        /// </summary>
        public virtual IEffectsStrategy FxManager
        {
            get;
            private set;
        }

        public virtual Starfield Starfield
        {
            get { return starfield; }
        }

        /// <summary>
        /// All ships that might enter the game.
        /// </summary>
        public virtual Ship[] Ships
        {
            get; private set;
        }

        public virtual Vector2[] Walls
        {
            get { return walls; }
        }

        /// <summary>
        /// Underlying representation of the world.
        /// </summary>
        /// <remarks>Needed for AI.</remarks>
        public virtual NavigationMesh NavigationMesh
        {
            get { return navigationMesh; }
        }

        /// <summary>
        /// All entities in the game.
        /// </summary>
        public virtual CollectCollection<IEntity> Entities
        { 
            get; private set;
        }

        /// <summary>
        /// All particle-systems in the game.
        /// </summary>
        public virtual CollectCollection<ParticleSystem> ParticleSystems
        { get;
            private set;
        }
        #endregion

        #region Initialization
        public World()
        {
        }

        /// <summary>
        /// Construct a new World object, holding the game simulation.
        /// </summary>
        public World(Vector2 dimensions)
        {
            this.dimensions = dimensions;
            safeDimensions = new Rectangle(0, 0, (int)(dimensions.X - 1), (int)(dimensions.Y - 1));
            
            CreatePlayers();
            CreateStartfield();

            // create a new list of entities
            Entities = new CollectCollection<IEntity>();

            //create effects manager
            FxManager = new BasicEffects(this);

            // create a new list of particle systems
            ParticleSystems = new CollectCollection<ParticleSystem>();
            
        }

        /// <summary>
        /// Create players for the game.
        /// </summary>
        private void CreatePlayers()
        {
            Ships = new Ship[4];
            Ships[0] = new LocalShip(this, PlayerIndex.One, shipColorsByPlayerIndex[0]);
			//Ships[0] = new BotShip(this, shipColorsByPlayerIndex[0]);
            Ships[1] = new BotShip(this, shipColorsByPlayerIndex[1]);
			Ships[2] = new BotShip(this, shipColorsByPlayerIndex[2]);
			Ships[3] = new BotShip(this, shipColorsByPlayerIndex[3]);
        }

        /// <summary>
        /// Create startfield using particle system.
        /// </summary>
        private void CreateStartfield()
        {
            starfield = new Starfield(starCount, new Rectangle(
               starfieldBuffer * -1,
               starfieldBuffer * -1,
               (int)dimensions.X + starfieldBuffer * 2,
               (int)dimensions.Y + starfieldBuffer * 2));
        }

        #endregion


        #region New Game
        public void StartNewGame()
        {
            // create the walls
            walls = WallFactory.CreateWalls(WorldRules.WallStyle, safeDimensions, out navigationMesh);

            // clear out the entities list
            Entities.Clear();

            // add the world actor
            worldEntity = new WorldEntity(this);
            Entities.Add(worldEntity);
            

            // add the players to the actor list - they won't be removed
            foreach (Ship ship in Ships) 
            {
                Entities.Add(ship);
            }

            // spawn asteroids
            AsteroidFactory.SpawnAsteroids(this, WorldRules.AsteroidDensity);

            // set up the power-up timer for the initial delay
            powerUpTimer = initialPowerUpDelay;

            // set up the starfield
            starfield.SetTargetPosition(dimensions * 0.5f);
        }


       
        /// <summary>
        /// Create a new power-up in the world, if possible
        /// </summary>
        void SpawnPowerUp()
        {
            // check if there is a powerup in the world
            var q = from e in Entities where e is PowerUp select e;
            if (q.Any()) return;
            
            // create the new power-up
            PowerUp powerup;
            switch (random.Next(3))
            {
                case 0:
                    powerup = new DoubleLaserPowerUp(this);
                    break;
                case 1:
                    powerup = new TripleLaserPowerUp(this);
                    break;
                default:
                    powerup = new RocketPowerUp(this);
                    break;
            }

            // add the new power-up to the world
            powerup.Spawn(true);
        }
        #endregion

        #region Update and Draw
        /// <summary>
        /// Update the world simulation.
        /// </summary>
        /// <param name="elapsedTime">The amount of elapsed time, in seconds.</param>
        public void Update(float elapsedTime)
        {
            // update all entities
            for (int i = 0; i < Entities.Count; i++)
            {
                IEntity entity = Entities[i];
                entity.Update(elapsedTime);
            }

            // update collision
            MoveWorld(elapsedTime);

            // update particle systems
            foreach (ParticleSystem particleSystem in ParticleSystems)
            {
                particleSystem.Update(elapsedTime);
                if (particleSystem.IsActive == false)
                {
                    ParticleSystems.Garbage.Add(particleSystem);
                }
            }

            // update the starfield
            Vector2 starfieldTarget = Vector2.Zero;
            int playingPlayers = 0;
            foreach(Ship ship in Ships) 
            {
                if (ship.Playing)
                {
                    starfieldTarget += ship.Position;
                    playingPlayers++;
                }
            }
            if (playingPlayers > 0)
            {
                starfield.SetTargetPosition(starfieldTarget / playingPlayers);
            }
            starfield.Update(elapsedTime);

            // check if we can create a new power-up yet
            if (powerUpTimer > 0f)
            {
                powerUpTimer = Math.Max(powerUpTimer - elapsedTime, 0f);
            }
            if (powerUpTimer <= 0.0f)
            {
                SpawnPowerUp();
                powerUpTimer = powerUpDelay;
            }

            // clean up the lists
            Entities.Collect();
            ParticleSystems.Collect();
        }


        /// <summary>
        /// Draw the walls.
        /// </summary>
        /// <param name="lineBatch">The LineBatch to render to.</param>
        public void DrawWalls(LineBatch lineBatch)
        {
            if (lineBatch == null)
            {
                throw new ArgumentNullException("lineBatch");
            }
            // draw each wall-line
            for (int wall = 0; wall < walls.Length / 2; wall++)
            {
                lineBatch.DrawLine(walls[wall * 2], walls[wall * 2 + 1], Color.Tomato);
            }

            if (WorldRules.ShowMeshes)
            {
                foreach (MeshNode mesh in navigationMesh.Meshes)
                {
                    for (int i = 0; i < mesh.Polygon.N - 1; i++)
                    {
                        lineBatch.DrawLine(mesh.Polygon[i], mesh.Polygon[i + 1], Color.WhiteSmoke);
                    }
                }
            }
        }
        #endregion

        #region Collision
        


        /// <summary>
        /// Overload for method below.
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns>True if a ray can be cast.</returns>
        public bool IsInLineOfSight(IEntity a, IEntity b)
        {
            return IsInLineOfSight(a.Position, b.Position);
        }

        /// <summary>
        /// Determine if there is "line of sight" between these two points. It only considers asteroids and walls as "obstacles".
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        public bool IsInLineOfSight(Vector2 a, Vector2 b)
        {
            //check asteroids
            foreach(IEntity entity in Entities)
            {
                if (!(entity is Asteroid)) continue;
                if (Collision.CircleLineCollide(entity.Position, entity.Radius, a, b))
                    return false;
            }

            //check walls
            for (var w = 0; w < walls.Length / 2; ++w)
            {
                if (Collision.LineLineIntersect(a, b, walls[w * 2], walls[w * 2 + 1]))
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Move all of the entities in the world.
        /// </summary>
        /// <param name="elapsedTime">The amount of elapsed time, in seconds.</param>
        private void MoveWorld(float elapsedTime)
        {
            Vector2 point = Vector2.Zero;
            // move each actor
            foreach (Entity entity in Entities) 
            {
                if (entity.Dead)
                {
                    continue;
                }
                // determine how far they are going to move
                Vector2 movement = entity.Velocity * elapsedTime;
                // only allow entities that have not collided yet this frame to collide
                // -- otherwise, objects can "double-hit" and trade their momentum fast
                if (entity.CollidedThisFrame == false)
                {
                    movement = MoveAndCollide(entity, movement);
                }

                // determine the new position
                entity.Position += movement;

                // determine if their new position takes them through a wall
                for (int w = 0; w < walls.Length / 2; ++w)
                {
                    if (entity is Projectile)
                    {
                        if (Collision.LineLineIntersect(entity.Position,
                            entity.Position - movement, walls[w * 2],
                            walls[w * 2 + 1], out point))
                        {
                            entity.Touch(worldEntity);
                        }
                    }
                    else
                    {
                        Collision.CircleLineCollisionResult result =
                            new Collision.CircleLineCollisionResult();
                        if (Collision.CircleLineCollide(entity.Position,
                            entity.Radius, walls[w * 2], walls[w * 2 + 1],
                            ref result))
                        {
                            // if a non-projectile hits a wall, bounce slightly
                            float vn = Vector2.Dot(entity.Velocity, result.Normal);
                            entity.Velocity -= (2.0f * vn) * result.Normal;
                            entity.Position += result.Normal * result.Distance;
                        }
                    }
                }
            }
        }


        /// <summary>
        /// Move the given entity by the given movement, colliding and adjusting
        /// as necessary.
        /// </summary>
        /// <param name="entity">The actor who is moving.</param>
        /// <param name="movement">The desired movement vector for this update.</param>
        /// <returns>The movement vector after considering all collisions.</returns>
        private Vector2 MoveAndCollide(Entity entity, Vector2 movement)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }
            // make sure we care about where this entity goes
            if (entity.Dead || (entity.Collidable == false))
            {
                return movement;
            }
            // make sure the movement is significant
            if (movement.LengthSquared() <= 0f)
            {
                return movement;
            }

            // generate the list of collisions
            Collide(entity, movement);

            // determine if we had any collisions
            if (collisionResults.Count > 0)
            {
                collisionResults.Sort(CollisionResult.Compare);
                foreach (CollisionResult collision in collisionResults)
                {
                    // let the two entities touch each other, and see what happens
                    if (entity.Touch(collision.entity) && collision.entity.Touch(entity))
                    {
                        entity.CollidedThisFrame =
                            collision.entity.CollidedThisFrame = true;
                        // they should react to the other, even if they just died
                        AdjustVelocities(entity, collision.entity);
                        return Vector2.Zero;
                    }
                }
            }

            return movement;
        }


        /// <summary>
        /// Determine all collisions that will happen as the given entity moves.
        /// </summary>
        /// <param name="entity">The actor that is moving.</param>
        /// <param name="movement">The entity's movement vector this update.</param>
        /// <remarks>The results are stored in the cached list.</remarks>
        private void Collide(Entity entity, Vector2 movement)
        {
            collisionResults.Clear();

            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }
            if (entity.Dead || (entity.Collidable == false))
            {
                return;
            }

            // determine the movement direction and scalar
            float movementLength = movement.Length();
            if (movementLength <= 0f)
            {
                return;
            }

            // check each entity
            foreach (Entity checkActor in Entities)
            {
                if ((entity == checkActor) || checkActor.Dead || !checkActor.Collidable)
                {
                    continue;
                }

                // calculate the target vector
                Vector2 checkVector = checkActor.Position - entity.Position;
                float distanceBetween = checkVector.Length() -
                    (checkActor.Radius + entity.Radius);

                // check if they could possibly touch no matter the direction
                if (movementLength < distanceBetween)
                {
                    continue;
                }

                // determine how much of the movement is bringing the two together
                float movementTowards = Vector2.Dot(movement, checkVector);

                // check to see if the movement is away from each other
                if (movementTowards < 0f)
                {
                    continue;
                }

                if (movementTowards < distanceBetween)
                {
                    continue;
                }

                CollisionResult result = new CollisionResult();
                result.Distance = distanceBetween;
                result.Normal = Vector2.Normalize(checkVector);
                result.entity = checkActor;

                collisionResults.Add(result);
            }
        }


        /// <summary>
        /// Adjust the velocities of the two entities as if they have collided,
        /// distributing their velocities according to their masses.
        /// </summary>
        /// <param name="actor1">The first actor.</param>
        /// <param name="actor2">The second actor.</param>
        private static void AdjustVelocities(IEntity actor1, IEntity actor2)
        {
            // don't adjust velocities if at least one has negative mass
            if ((actor1.Mass <= 0f) || (actor2.Mass <= 0f))
            {
                return;
            }

            // determine the vectors normal and tangent to the collision
            Vector2 collisionNormal = Vector2.Normalize(
                actor2.Position - actor1.Position);
            Vector2 collisionTangent = new Vector2(
                -collisionNormal.Y, collisionNormal.X);

            // determine the velocity components along the normal and tangent vectors
            float velocityNormal1 = Vector2.Dot(actor1.Velocity, collisionNormal);
            float velocityTangent1 = Vector2.Dot(actor1.Velocity, collisionTangent);
            float velocityNormal2 = Vector2.Dot(actor2.Velocity, collisionNormal);
            float velocityTangent2 = Vector2.Dot(actor2.Velocity, collisionTangent);

            // determine the new velocities along the normal
            float velocityNormal1New = ((velocityNormal1 * (actor1.Mass - actor2.Mass))
                + (2f * actor2.Mass * velocityNormal2)) / (actor1.Mass + actor2.Mass);
            float velocityNormal2New = ((velocityNormal2 * (actor2.Mass - actor1.Mass))
                + (2f * actor1.Mass * velocityNormal1)) / (actor1.Mass + actor2.Mass);

            // determine the new total velocities
            actor1.Velocity = (velocityNormal1New * collisionNormal) +
                (velocityTangent1 * collisionTangent);
            actor2.Velocity = (velocityNormal2New * collisionNormal) +
                (velocityTangent2 * collisionTangent);
        }


        /// <summary>
        /// Find a valid point for the entity to spawn.
        /// </summary>
        /// <param name="entity">The actor to find a location for.</param>
        /// <remarks>This query is not bounded, which would be needed in a more complex
        /// game with a likelihood of no valid spawn locations.</remarks>
        /// <returns>A valid location for the user to spawn.</returns>
        public Vector2 FindSpawnPoint(Entity entity)
        {
            if (entity == null)
            {
                throw new ArgumentNullException("entity");
            }

            Vector2 spawnPoint;
            // fudge the radius slightly so we're not right on top of another entity
            float radius = entity is Ship ? entity.Radius * 2f : entity.Radius*1.1f;
            radius = (float)Math.Ceiling(radius);

            Vector2 spawnMinimum = new Vector2(
                safeDimensions.X + radius,
                safeDimensions.Y + radius);

            Vector2 spawnDimensions = new Vector2(
                (float)Math.Floor(safeDimensions.Width - 2f * radius),
                (float)Math.Floor(safeDimensions.Height - 2f * radius));

            Vector2 spawnMaximum = spawnMinimum + spawnDimensions;

            Collision.CircleLineCollisionResult result =
                new Collision.CircleLineCollisionResult();
            
            while (true)
            {
                bool valid = true;
                // generate a new spawn point
                spawnPoint = new Vector2(
                    spawnMinimum.X + spawnDimensions.X * (float)random.NextDouble(),
                    spawnMinimum.Y + spawnDimensions.Y * (float)random.NextDouble());
                if ((spawnPoint.X < spawnMinimum.X) ||
                    (spawnPoint.Y < spawnMinimum.Y) ||
                    (spawnPoint.X > spawnMaximum.X) ||
                    (spawnPoint.Y > spawnMaximum.Y))
                {
                    continue;
                }
                // if we don't collide, then one is good enough
                if (entity.Collidable == false)
                {
                    break;
                }
                
                // check against the walls
                for (int wall = 0; wall < walls.Length / 2; wall++)
                {
                    if (Collision.CircleLineCollide(spawnPoint, radius,
                        walls[wall * 2], walls[wall * 2 + 1], ref result))
                    {
                        valid = false;
                        break;
                    }
                }

                // check against all other entities
                if (valid)
                {
                    foreach (Entity checkActor in Entities)
                    {
                        if ((entity == checkActor) || checkActor.Dead)
                        {
                            continue;
                        }
                        if (Collision.CircleCircleIntersect(checkActor.Position,
                            checkActor.Radius, spawnPoint, radius))
                        {
                            valid = false;
                            break;
                        }
                    }
                }
                // if we have gotten this far, then the spawn point is good
                if (valid)
                {
                    break;
                }
            }

            return spawnPoint;
        }
        #endregion
    }
}



